﻿//    CMake Classifier:  Syntax coloring and highlighting Visual Studio Extension
//    Copyright (C) 2011 Steven Velez
//
//    This file is part of CMake Classifier
//
//    CMake Classifier is free software: you can redistribute it and/or modify
//    it under the terms of the GNU Lesser General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    (at your option) any later version.
//
//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU Lesser General Public License for more details.
//
//    You should have received a copy of the GNU Lesser General Public License
//    along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
using CMakeParsing.Interface;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Classification;

namespace CMakeClassifier
{
    #region Classifier
    /// <summary>
    /// Classifier that classifies all text as an instance of the OrinaryClassifierType
    /// </summary>
    class CMakeClassifier : IClassifier
    {
        bool _bActive = false;
        ITextBuffer _buff = null;
        IClassificationTypeRegistryService _registry;

        SyntaxStyleModel _model = null;
        LinkedList<SnapshotSpan> _pending = null;

        [Import]
        internal IEditorFormatMapService FormatMapService { get; set; }

        internal CMakeClassifier(IClassificationTypeRegistryService registry, ITextBuffer buffer)
        {
            _registry = registry;
            _buff = buffer;
            _buff.Changed += new EventHandler<TextContentChangedEventArgs>(_buff_Changed);

            ITextDocument doc;
            if (buffer.Properties.TryGetProperty(typeof(ITextDocument), out doc))
            {
                doc.FileActionOccurred += new EventHandler<TextDocumentFileActionEventArgs>(doc_FileActionOccurred);
                _setActivation(doc.FilePath, false);
            }
        }

        /// <summary>
        /// This method scans the given SnapshotSpan for potential matches for this classification.
        /// In this instance, it classifies everything and returns each span as a new ClassificationSpan.
        /// </summary>
        /// <param name="trackingSpan">The span currently being classified</param>
        /// <returns>A list of ClassificationSpans that represent spans identified to be of this classification</returns>
        public IList<ClassificationSpan> GetClassificationSpans(SnapshotSpan span)
        {
            //create a list to hold the results
            List<ClassificationSpan> classifications = new List<ClassificationSpan>();

            // Abort if inactive
            if (!_bActive) { return classifications; }

            bool bCleared = clearPendingSpans(span);

            if (bCleared)
            {
                // Make sure there are not pending spans before the current ones, 
                // otherwise we may get the wrong context information.
                LinkedListNode<SnapshotSpan> pCurr = _pending.First;
                while (null != pCurr && (pCurr.Value.End <= span.Start))
                {
                    SnapshotSpan preSpan = pCurr.Value;
                    pCurr = pCurr.Next;
                    _model.TokensFor(preSpan.Start, preSpan.GetText(), true);
                    clearPendingSpans(preSpan);
                }
            }

            foreach (StyleToken tok in _model.TokensFor(span.Start, span.GetText(), bCleared))
            {
                classifications.Add(new ClassificationSpan(new SnapshotSpan(span.Snapshot, new Span(tok.startIdx, tok.length)),
                                                            _registry.GetClassificationType(tok.styleClass)));
            }

            // Let the environment know that stuff after this needs updating.
            if (bCleared && span.End < span.Snapshot.Length && null != ClassificationChanged)
            {
                // mark the remainder of the document as having unknown state.
                SnapshotSpan changed = new SnapshotSpan(span.End, span.Snapshot.Length - span.End);
                clearPendingSpans(changed);
                _model.EraseRange(span.End, span.Snapshot.Length, 0);
                _pending.AddLast(changed);

                // But only tell VS that the next line needs parsing... and get the rest of the
                // (visible) document in a cascade effect.  Otherwise we scan WAY too much.
                ITextSnapshotLine line = changed.Snapshot.GetLineFromPosition(span.End);
                ClassificationChangedEventArgs args = new ClassificationChangedEventArgs(new SnapshotSpan(
                    line.Start, line.LengthIncludingLineBreak));
                ClassificationChanged(this, args);
            }

            return classifications;
        }

#pragma warning disable 67
        // This event gets raised if a non-text change would affect the classification in some way,
        // for example typing /* would cause the classification to change in C# without directly
        // affecting the span.
        public event EventHandler<ClassificationChangedEventArgs> ClassificationChanged;
#pragma warning restore 67

        //
        // Helpers
        //

        /// <summary>
        /// Decide whether classification active state needs to change and change it.
        /// </summary>
        /// <param name="filePath">The path of the file for the buffer.  Determines whether it should be active</param>
        /// <param name="bNotify">if true will trigger a re-classifcation of the buffer</param>
        private void _setActivation(string filePath, bool bNotify = true)
        {
            const string kCmakeExt = ".cmake";
            const string kCtestExt = ".ctest";
            const string kInExt = ".in";
            const string kCMakeListsName = "cmakelists.txt";
            string baseName;

            bool bShouldBeActive = (Path.GetFileName(filePath).ToLower() == kCMakeListsName) ||
                (Path.GetExtension(filePath).ToLower() == kCmakeExt) ||
                (Path.GetExtension(filePath).ToLower() == kCtestExt) ||
                (Path.GetExtension(filePath).ToLower() == kInExt  && (null != (baseName = Path.GetFileNameWithoutExtension(filePath))) &&
                        ( (Path.GetExtension(baseName) == kCmakeExt) ||
                          (Path.GetExtension(baseName) == kCtestExt) ));

            if (bShouldBeActive != _bActive)
            {
                if (_bActive)
                {
                    // Was active, so deactivate
                    _model = null;
                    _pending = null;
                }
                else
                {
                    // Wasn't active, so activate
                    _model = new SyntaxStyleModel();
                    _pending = new LinkedList<SnapshotSpan>();

                    ITextSnapshot snapshot = _buff.CurrentSnapshot;
                    if (snapshot.Length > 0)
                    {
                        // The whole thing needs to be scanned
                        _pending.AddLast(new SnapshotSpan(snapshot, 0, snapshot.Length));
                    }
                }

                _bActive = bShouldBeActive;
                if (bNotify && (null != ClassificationChanged))
                {
                    ClassificationChanged(this, new ClassificationChangedEventArgs(
                        new SnapshotSpan(_buff.CurrentSnapshot, 0, _buff.CurrentSnapshot.Length)
                        ));
                }
            }
        }

        /// <summary>
        /// Remove a span from the pending spans list
        /// </summary>
        /// <remarks>
        /// If a span is not in the pending spans list, then this means it has not been
        /// scanned yet.  Otherwise, it should have been. (the default value for 
        /// bRemoveSpanFromPending) helps to make this so.
        /// </remarks>
        /// <param name="span">the span to search for</param>
        /// <returns>true when the span is found in the pending list</returns>
        private bool clearPendingSpans(SnapshotSpan span)
        {
            bool bRetVal = false;

            LinkedListNode<SnapshotSpan> pCurr = _pending.First;

            while (null != pCurr && (span.End >= pCurr.Value.Start) )
            {
                SnapshotSpan curPending = pCurr.Value.TranslateTo(span.Snapshot, SpanTrackingMode.EdgeExclusive);
                LinkedListNode<SnapshotSpan> pNext = pCurr.Next;  // Remember the next node, in case, current is removed.
                if (span.OverlapsWith(curPending))
                {
                    // Found in the pending regions, so this means
                    // that it hasn't been scanned yet.
                    bRetVal = true;

                    {
                        // Find the point at which the pending and the new intersect
                        SnapshotSpan? intersection = span.Intersection(curPending);

                        // And create new spans for everything that is not included therein
                        int pfxLen = intersection.Value.Start - curPending.Start;
                        int sfxLen = curPending.End - intersection.Value.End;
                        if (sfxLen > 0)
                        {
                            _pending.AddAfter(pCurr, new SnapshotSpan(intersection.Value.End, curPending.End));
                        }
                        if (pfxLen > 0)
                        {
                            _pending.AddBefore(pCurr, new SnapshotSpan(curPending.Start, intersection.Value.Start));
                        }

                        // Finally, remove the old pending span.
                        _pending.Remove(pCurr);
                    }
                }

                pCurr = pNext;
            }
            
            return bRetVal;
        }

        //
        // Event Handlers
        //

        /// <summary>
        /// Listen for document renames to update active state.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void doc_FileActionOccurred(object sender, TextDocumentFileActionEventArgs e)
        {
            if ((e.FileActionType & FileActionTypes.DocumentRenamed) == FileActionTypes.DocumentRenamed)
            {
                _setActivation(e.FilePath);
            }
        }


        /// <summary>
        /// Update the state of the classifier and the model when the buffer 
        /// contents change.
        /// </summary>
        void _buff_Changed(object sender, TextContentChangedEventArgs e)
        {
            // Abort if inactive
            if (!_bActive) { return; }

            foreach (ITextChange change in e.Changes)
            {
                ITextSnapshotLine oldStartLine = e.Before.GetLineFromPosition(change.OldPosition);
                ITextSnapshotLine oldEndLine = e.Before.GetLineFromPosition(change.OldEnd);
                ITextSnapshotLine newStartLine = e.After.GetLineFromPosition(change.NewPosition);
                ITextSnapshotLine newEndLine = e.After.GetLineFromPosition(change.NewEnd);

                {
                    // Since classifications are requested a line at a time, we need to remove old data
                    // in line-chunks.
                    int changeStart = oldStartLine.Start;
                    int changeEnd = oldEndLine.Start + oldEndLine.LengthIncludingLineBreak;
                    _model.EraseRange(changeStart, changeEnd, change.Delta);

                    // Remove the change from anything that may still be pending.
                    SnapshotSpan oldSpan = new SnapshotSpan(oldStartLine.Start, oldEndLine.EndIncludingLineBreak);
                    clearPendingSpans(oldSpan);
                    // update the location of the spans
                    LinkedListNode<SnapshotSpan> pCurr = _pending.First;
                    LinkedListNode<SnapshotSpan> pInserted = null;
                    SnapshotSpan newSpan = new SnapshotSpan(newStartLine.Start, newEndLine.EndIncludingLineBreak);
                    while (null != pCurr)
                    {
                        if (pCurr.Value.Start >= oldSpan.End)
                        {
                            pCurr.Value = pCurr.Value.TranslateTo(e.After, SpanTrackingMode.EdgePositive);
                            if (null == pInserted)
                            {
                                pInserted = _pending.AddBefore(pCurr, newSpan);
                            }
                        }
                        pCurr = pCurr.Next;
                    }

                    // Merge up spans for efficiency when possible
                    if (null != pInserted)
                    {
                        int start = newSpan.Start, end = newSpan.End;
                        // Check to see if we can hook up with the span ahead
                        if (null != (pCurr = pInserted.Previous) &&
                            (pCurr.Value.End == newSpan.Start))
                        {
                            start = pCurr.Value.Start;
                            _pending.Remove(pCurr);
                        }
                        // Check for connecting to the span following
                        if (null != (pCurr = pInserted.Next) &&
                            (pCurr.Value.Start == newSpan.End))
                        {
                            end = pCurr.Value.End;
                            _pending.Remove(pCurr);
                        }

                        // Make a new span to include multiples
                        if ((start != newSpan.Start) ||
                            (end != newSpan.End))
                        {
                            _pending.AddAfter(pInserted, new SnapshotSpan(e.After, start, end - start));
                            _pending.Remove(pInserted);
                        }
                    }
                    else
                    {
                        if (_pending.Last != null &&
                            _pending.Last.Value.End.Position == newSpan.Start.Position)
                        {
                            _pending.Last.Value = new SnapshotSpan(e.After, _pending.Last.Value.Start,
                                _pending.Last.Value.Length + newSpan.Length);
                        }
                        else if (newSpan.Length > 0)
                        {
                            _pending.AddLast(newSpan);
                        }
                    }


                }
            }
        }
    }
    #endregion //Classifier
}
